package junior.db.dao.base;

import com.google.common.collect.Lists;
import junior.db.dao.enums.LikeMode;
import junior.db.dao.enums.Operator;
import junior.db.dao.query.AbstractQuery;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * 所有过滤型Param的基类，主要提供过滤相关方法，以及groupBy和having等操作
 * @param <Q>
 */
@SuppressWarnings("unchecked")
public abstract class AbstractFilter<Q> implements IFilter<Q>, IEntityWrapper, INamespaceAware, IExecuteAware, IEntityInject {
    private boolean immutable;
    private boolean skipNullValueFilter;
    private FilterValueValidator validator = new SimpleFilterValueValidator();
    
    protected Class<?> entityClass;
    protected String prefix;
    protected String table;
    
    protected List<IFilterItem> filters;
    protected IFilterItem lastFilter;
    protected List<SubQuery> subQueryList;
    
    protected String namespace;
    protected List<String> groupBies;
    protected List<Filter> havings;
    protected List<String> havingCustoms;
    private int inBatchSize = 1000;
    
    protected AbstractFilter() {}
    
    protected AbstractFilter(boolean immutable) {
        this.immutable = immutable;
    }
    
    @Override
    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }
    
    public String getNamespace() {
        return namespace;
    }
    
    public Q fromFilterParam(AbstractFilter<?> param) {
        List<IFilterItem> paramFilters = param.filters;
        if (paramFilters != null && !paramFilters.isEmpty()) {
            this.setFilters(new ArrayList<>(paramFilters));
        }
        this.setNamespace(param.namespace);
        this.setImmutable(param.isImmutable());
        this.withSkipNullValueFilter(param.isSkipNullValueFilter());
        if (this.getEntityClass() == null && param.getEntityClass() != null) {
            this.setEntityClass(param.getEntityClass());
        }
        if (this.getPrefix() == null && param.getPrefix() != null) {
            this.withPrefix(param.getPrefix());
        }
        this.subQueryList = param.getSubQueryList();
        this.groupBies = param.groupBies;
        this.havings = param.havings;
        this.havingCustoms = param.havingCustoms;
        return (Q) this;
    }
    
    public void setInBatchSize(int inBatchSize) {
        this.inBatchSize = inBatchSize;
    }
    
    public boolean isImmutable() {
        return immutable;
    }
    
    protected void setImmutable(boolean immutable) {
        this.immutable = immutable;
    }
    
    public void setEntityClass(Class<?> entityClass) {
        this.entityClass = entityClass;
        if (table == null) {
            table = MybatisHelper.getTable(namespace, this.entityClass);
        }
    }
    
    public Q withSkipNullValueFilter(boolean skipNullValueFilter) {
        this.skipNullValueFilter = skipNullValueFilter;
        return (Q) this;
    }
    
    protected boolean isSkipNullValueFilter() {
        return skipNullValueFilter;
    }
    
    public Q withValidator(FilterValueValidator validator) {
        this.validator = validator;
        return (Q) this;
    }
    
    public Q withPrefix(String defaultPrefix) {
        this.prefix = defaultPrefix;
        return (Q) this;
    }
    
    public Q withTable(String table) {
        this.table = table;
        return (Q) this;
    }
    
    public String getTable() {
        return table;
    }
    
    public String getPrefix() {
        return prefix;
    }
    
    public Class<?> getEntityClass() {
        return entityClass;
    }
    
    public boolean hasFilter(String field) {
        if (filters == null) {
            return false;
        }
        
        for (IFilterItem f : filters) {
            if (!f.isNormal()) {
                continue;
            }
            Filter filter = (Filter) f;
            if (field.equals(filter.getField())) {
                return true;
            }
        }
        return false;
    }
    
    public boolean hasAnyFilter() {
        if (filters == null) return false;
        
        return ! filters.isEmpty();
    }
    
    public int getFilterCount() {
        return filters != null ? filters.size() : 0;
    }
    
    public List<IFilterItem> getFilters() {
        return getFilterItems(false);
    }
    
    protected List<IFilterItem> getFilterItems(boolean containsSubQuery) {
        if (filters != null) {
            List<IFilterItem> _filters = new ArrayList<>();
            for (IFilterItem fi : filters) {
                if (!fi.isNormal()) {
                    if (containsSubQuery) {
                        _filters.add(fi);
                    }
                    continue;
                }
                Filter filter = (Filter) fi;
                if (filter.isLogic()) {
                    _filters.add(filter);
                    continue;
                }
                String field = filter.getField();
                if (field == null) {
                    _filters.add(filter);
                    continue;
                }
                
                if (StringUtils.isEmpty(filter.getPrefix()) && ! hasPrefix(field)) {
                    String prefix = this.prefix;
                    if (prefix != null) {
                        filter.setPrefix(appendDot2Prefix(prefix));
                    }
                }
                if (filter.isLeftAsExpression()) {
                    filter.setLeft(MybatisHelper.getCustomSql(namespace, field, filter.getPrefix(), this::getEntityClassByPrefix));
                } else {
                    filter.setLeft(MybatisHelper.field2Column(namespace, field, filter.getPrefix(), this::getEntityClassByPrefix));
                }
                if (filter.isRightAsField()) {
                    String rightField = (String) filter.getRight();
                    String column = MybatisHelper.field2Column(namespace, rightField, filter.getPrefix(), this::getEntityClassByPrefix);
                    filter.setRightIsExpression(new SqlValue().sql(column));
                    filter.withRightAsExpression();
                } else if (filter.getRightIsExpression()) {
                    String sql = (String) filter.getRight();
                    if (sql != null) {
                        SqlValue sqlValue = MybatisHelper.sqlToSqlValue(namespace, getEntityClass(), sql, (f) -> getFilterArgFieldValue(filter, f));
                        filter.setRightIsExpression(sqlValue);
                    }
                }
                _filters.add(filter);
            }
            return _filters;
        }
        return Collections.emptyList();
    }
    
    protected void setFilters(List<IFilterItem> filters) {
        this.filters = filters;
    }
    
    public Q eq(String left, Object value) {
        return addFilter(Filter.eq(left, value));
    }
    
    public Q neq(String left, Object value) {
        return addFilter(Filter.neq(left, value));
    }
    
    public Q gt(String left, Object value) {
        return addFilter(Filter.gt(left, value));
    }
    
    public Q lt(String left, Object value) {
        return addFilter(Filter.lt(left, value));
    }
    
    public Q gte(String left, Object value) {
        return addFilter(Filter.gte(left, value));
    }
    
    public Q lte(String left, Object value) {
        return addFilter(Filter.lte(left, value));
    }
    
    public Q bt(String left, Object value1, Object value2) {
        return addFilter(Filter.bt(left, value1, value2));
    }
    
    public Q nvl(String left) {
        return addFilter(Filter.nvl(left));
    }
    
    public Q notNvl(String left) {
        return addFilter(Filter.notNvl(left));
    }
    
    public Q in(String left, Collection<?> values) {
        Assert.notEmpty(values, "Parameter values cannot be null or empty");
        doBatch(left, Operator.IN, values.toArray());
        return (Q) this;
    }
    
    public Q in(String left, Object[] values) {
        Assert.notEmpty(values, "Parameter values cannot be null or empty");
        doBatch(left, Operator.IN, values);
        return (Q) this;
    }
    
    public Q notIn(String left, Collection<?> values) {
        Assert.notEmpty(values, "Parameter values cannot be null or empty");
        doBatch(left, Operator.NOT_IN, values.toArray());
        return (Q) this;
    }
    
    public Q notIn(String left, Object[] values) {
        Assert.notEmpty(values, "Parameter values cannot be null or empty");
        doBatch(left, Operator.NOT_IN, values);
        return (Q) this;
    }
    
    private void doBatch(String left, Operator operator, Object[] values) {
        if (values.length > inBatchSize) {
            addFilter(Filter.leftBracket());
            int remain = values.length;
            int pos = 0;
            while (remain > 0) {
                int subSize = Math.min(inBatchSize, remain);
                Object[] sub = new Object[subSize];
                System.arraycopy(values, pos, sub, 0, sub.length);
                if (pos > 0) {
                    addFilter(Filter.or());
                }
                addFilter(new Filter(left, operator, sub));
                pos += subSize;
                remain -= subSize;
            }
            addFilter(Filter.rightBracket());
        } else {
            addFilter(new Filter(left, operator, values));
        }
    }
    
    public Q like(String left, Object value, LikeMode likeMode) {
        if (!checkLikeValue(value)) {
            return (Q) this;
        }
        return addFilter(Filter.like(left, value, likeMode));
    }
    
    public Q notLike(String left, Object value, LikeMode likeMode) {
        if (!checkLikeValue(value)) {
            return (Q) this;
        }
        return addFilter(Filter.notLike(left, value, likeMode));
    }
    
    public Q filterField(String left, Operator operator, String right) {
        return filterField(left, operator, right, false);
    }
    
    public Q filterField(String left, Operator operator, String right, boolean rightAsField) {
        if (rightAsField) {
            return addFilter(new Filter(left, operator, right).withRightAsField());
        }
        return addFilter(new Filter(left, operator, right).withRightAsExpression());
    }
    
    public Q and() {
        return addFilter(Filter.and());
    }
    
    public Q or() {
        return addFilter(Filter.or());
    }
    
    public Q leftBracket() {
        return addFilter(Filter.leftBracket());
    }
    
    public Q rightBracket() {
        return addFilter(Filter.rightBracket());
    }
    
    /**
     * exists过滤
     * @query query
     * @return
     */
    public <S extends AbstractQuery & IEntityTypeAssigned> QueryLink<Q, S> exists(S query) {
        String selectField = null;
        if (query.getSelectFields() != null && query.getSelectFields().length == 1) {
            selectField = query.getSelectFields()[0];
        } else {
            query.selectExpression("1", null);
        }
        return doSubQuery("exists", this, query, null, selectField);
    }
    
    public <S extends AbstractQuery & IEntityTypeAssigned> QueryLink<Q, S> notExists(S query) {
        String selectField = null;
        if (query.getSelectFields() != null && query.getSelectFields().length == 1) {
            selectField = query.getSelectFields()[0];
        } else {
            query.selectExpression("1", null);
        }
        return doSubQuery("not exists", this, query, null, selectField);
    }
    
    public <S extends AbstractQuery & IEntityTypeAssigned> QueryLink<Q, S> in(S query, String filterField, String selectField) {
        return doSubQuery(Operator.IN.getOptString(), this, query, filterField, selectField);
    }
    
    public <S extends AbstractQuery & IEntityTypeAssigned> QueryLink<Q, S> notIn(S query, String filterField, String selectField) {
        return doSubQuery(Operator.NOT_IN.getOptString(), this, query, filterField, selectField);
    }
    
    public <S extends AbstractQuery & IEntityTypeAssigned> QueryLink<Q, S> subQuery(Operator operator, S query, String filterField, String selectField) {
        return doSubQuery(operator.getOptString(), this, query, filterField, selectField);
    }
    
    protected <S extends AbstractQuery & IEntityTypeAssigned> QueryLink<Q, S> doSubQuery(String operator, AbstractFilter main, S query, String filterField, String selectField) {
        SubQuery subQuery = new SubQuery(query);
        QueryLink<Q, S> ref = new QueryLink<>((Q) main, query);
        if (selectField != null) {
            query.select(selectField);
        }
        
        addFilter(new SubQueryFilter(filterField, operator, subQuery));
        
        if (subQueryList == null) {
            subQueryList = Lists.newArrayList();
        }
        subQueryList.add(subQuery);
        
        return ref;
    }
    
    public final List<SubQuery> getSubQueryList() {
        return subQueryList;
    }
    
    /**
     * or 多个过滤器，自动加上括号
     * @param filters
     * @return
     */
    public Q orBracket(Filter... filters) {
        bracket(Filter.or(), filters);
        return (Q) this;
    }
    
    /**
     * and 多个过滤器，自动加上括号
     * @param filters
     * @return
     */
    public Q andBracket(Filter... filters) {
        bracket(Filter.and(), filters);
        return (Q) this;
    }
    
    public Q groupBy(String... fields) {
        Assert.notEmpty(fields, "Parameter fields is required");
        
        if (groupBies == null) {
            groupBies = Lists.newArrayList();
        }
        for (String field : fields) {
            groupBies.add(field);
        }
        
        return (Q) this;
    }
    
    public Q having(Filter filter) {
        Assert.notNull(filter, "Parameter filter is required");
        if (havings == null) {
            havings = Lists.newArrayList();
        }
        this.havings.add(filter);
        return (Q) this;
    }
    
    public Q having(String field, Operator operator, Object value) {
        Assert.notNull(field, "Parameter field is required");
        Assert.notNull(operator, "Parameter operator is required");
        Assert.notNull(value, "Parameter value is required");
        if (havings == null) {
            havings = Lists.newArrayList();
        }
        this.havings.add(new Filter(field, operator, value));
        
        return (Q) this;
    }
    
    public Q having(String having) {
        Assert.notNull(having, "Parameter having is required");
        
        if (havingCustoms == null) {
            havingCustoms = Lists.newArrayList();
        }
        
        this.havingCustoms.add(having);
        return (Q) this;
    }
    
    public List<String> getGroupBies() {
        List<String> sqls = Lists.newArrayList();
        if (groupBies != null) {
            for (String field : groupBies) {
                sqls.add(MybatisHelper.field2Column(namespace, field, getPrefix(), this::getEntityClassByPrefix));
            }
            return sqls;
        }
        return sqls.isEmpty() ? null : sqls;
    }
    
    public List<String> getHavings() {
        List<String> sqls = Lists.newArrayList();
        if (havings != null || havingCustoms != null) {
            if (havings != null) {
                for (Filter filter : havings) {
                    String column = MybatisHelper.field2Column(namespace, filter.getField(), this::getEntityClassByPrefix);
                    sqls.add(column + filter.getOperator().getOptString() + filter.getRight());
                }
            }
            if (havingCustoms != null) {
                for (String having : havingCustoms) {
                    String column = MybatisHelper.getCustomSql(namespace, having, getPrefix(), this::getEntityClassByPrefix);
                    if (! sqls.contains(column)) {
                        sqls.add(column);
                    }
                }
            }
        }
        return sqls.isEmpty() ? null : sqls;
    }
    
    void setGroupBies(List<String> groupBies) {
        this.groupBies = groupBies;
    }
    void setHavings(List<Filter> havings) {
        this.havings = havings;
    }
    void setHavingCustoms(List<String> havingCustoms) {
        this.havingCustoms = havingCustoms;
    }
    
    protected void bracket(Filter joinFilter, Filter... filters) {
        boolean[] checkRslts = new boolean[filters.length];
        int validFilterCount = 0;
        for (int i=0; i<filters.length; i++) {
            checkRslts[i] = checkFilter(filters[i]);
            if (checkRslts[i]) validFilterCount ++;
        }
        
        if (validFilterCount > 1) leftBracket();
        
        for (int i=0; i<filters.length; i++) {
            if (checkRslts[i]) {
                if (i > 0) addFilter(joinFilter);
                
                addFilter(filters[i]);
            }
        }
        
        if (validFilterCount > 1) rightBracket();
    }
    
    public Q addFilter(IFilterItem filter) {
        if (! checkFilter(filter)) return (Q) this;
        
        prepareFilters();
        
        IFilterItem _lastFilter = lastFilter;
        
        if (_lastFilter == null && !filters.isEmpty()) {
            _lastFilter = filters.get(filters.size() - 1);
        }
        
        if (_lastFilter != null && _lastFilter.isRequirePostJoinOpt() && filter.isRequirePreJoinOpt()) {
            filters.add(Filter.and());
        }
        filters.add(filter);
        lastFilter = filter;
        
        return (Q) this;
    }
    
    public Q removeFilter(String field) {
        if (filters == null) {
            return (Q) this;
        }
        List<Filter> requiredRemoved = new ArrayList<>();
        int idx = 0;
        boolean inBracket = false;
        List<Filter> inBracketFilters = new ArrayList<>();
        for (IFilterItem fi : filters) {
            if (!fi.isNormal()) {
                continue;
            }
            Filter f = (Filter) fi;
            if (f.getOperator() == Operator.LEFT_BRACKET) {
                inBracket = true;
            } else if (f.getOperator() == Operator.RIGHT_BRACKET) {
                inBracketFilters.stream().filter(ibf -> !requiredRemoved.contains(ibf)).findFirst()
                        .filter(ibf -> !ibf.isRequirePreJoinOpt()).ifPresent(ibf -> requiredRemoved.add(ibf));
                inBracket = false;
            } else if (inBracket) {
                inBracketFilters.add(f);
            }
            if (field.equals(f.getField())) {
                requiredRemoved.add(f);
                if (idx > 0) {
                    IFilterItem lastFi = filters.get(idx - 1);
                    if (lastFi.isNormal() ) {
                        Filter last = (Filter) lastFi;
                        if (last.isLogic() && last.getOperator() == Operator.LEFT_BRACKET && idx < filters.size() - 1) {
                            IFilterItem nextFi = filters.get(idx + 1);
                            if (nextFi.isNormal()) {
                                Filter next = (Filter) nextFi;
                                if (next.getOperator() != Operator.RIGHT_BRACKET) {
                                    requiredRemoved.add(next);
                                }
                            }
                        } else {
                            requiredRemoved.add(last);
                        }
                    }
                }
            }
            idx ++;
        }
        idx = 0;
        for (Iterator<IFilterItem> ite = filters.iterator(); ite.hasNext(); ) {
            IFilterItem f = ite.next();
            if (requiredRemoved.contains(f)) {
                ite.remove();
            }
            idx ++;
        }
        if (!filters.isEmpty()) {
            IFilterItem firstFi = filters.get(0);
            if (firstFi.isNormal()) {
                Filter first = (Filter) firstFi;
                if (first.isLogic() && !first.isRequirePreJoinOpt()) {
                    filters.remove(0);
                }
            }
        }
        removeEmptyBrackets();
        lastFilter = !filters.isEmpty() ? filters.get(filters.size() - 1) : null;
        return (Q) this;
    }
    
    /**
     * 是否是一个复杂的过滤器
     * @param field
     * @return
     */
    public boolean isFilterComplicated(String field) {
        if (filters != null) {
            boolean notMatchedInBracket = false;
            boolean matchedInBracket = false;
            int inBracket = 0;
            for (IFilterItem fi : filters) {
                if (!fi.isNormal()) {
                    return true;
                }
                Filter f = (Filter) fi;
                if (f.getOperator() == Operator.LEFT_BRACKET) {
                    inBracket ++;
                } else if (inBracket > 0 && f.getOperator() == Operator.RIGHT_BRACKET) {
                    inBracket --;
                    if (inBracket == 0) { // reset
                        notMatchedInBracket = false;
                        matchedInBracket = false;
                    }
                } else if (!f.isLogic() && field.equals(f.getField())) {
                    if (notMatchedInBracket) {
                        return true;
                    }
                    if (inBracket > 0) {
                        matchedInBracket = true;
                    }
                } else if (!f.isLogic()) {
                    if (matchedInBracket) {
                        return true;
                    }
                    if (inBracket > 0) {
                        notMatchedInBracket = true;
                    }
                }
            }
        }
        return false;
    }
    
    public List<Filter> getFiltersByField(String field) {
        List<Filter> result = new ArrayList<>();
        if (filters != null) {
            List<Filter> middleFilters = new ArrayList<>();
            boolean matches = false;
            int idx = 0;
            boolean inBracket = false;
            int firstInBracketIndex = -1;
            int inBracketFilterCount = 0;
            for (IFilterItem fi : filters) {
                if (!fi.isNormal()) {
                    continue;
                }
                Filter f = (Filter) fi;
                if (f.getOperator() == Operator.LEFT_BRACKET) {
                    inBracket = true;
                } else if (f.getOperator() == Operator.RIGHT_BRACKET) {
                    if (inBracket && inBracketFilterCount > 1) {
                        result.add(firstInBracketIndex > 0 ? firstInBracketIndex - 1 : 0, Filter.leftBracket());
                        result.add(Filter.rightBracket());
                    }
                    inBracketFilterCount = 0;
                    inBracket = false;
                    firstInBracketIndex = -1;
                }
                if (!f.isLogic() && field.equals(f.getField())) {
                    if (middleFilters.stream().allMatch(Filter::isLogic)) {
                        result.addAll(middleFilters);
                        middleFilters.clear();
                    }
    
                    if (idx > 0 && (result.isEmpty() || result.get(result.size() - 1).isRequirePostJoinOpt())) {
                        IFilterItem lastFi = filters.get(idx - 1);
                        if (lastFi.isNormal()) {
                            Filter last = (Filter) lastFi;
                            if (last.isLogic() && last.getOperator() != Operator.LEFT_BRACKET) {
                                result.add(last);
                            }
                        }
                    }
                    result.add(f);
                    if (inBracket) {
                        if (firstInBracketIndex == -1) {
                            firstInBracketIndex = result.size() - 1;
                        }
                        inBracketFilterCount ++;
                    }
                    matches = true;
                } else if (matches) {
                    middleFilters.add(f);
                    matches = false;
                }
                idx ++;
            }
        }
        return result;
    }
    
    protected boolean checkFilter(IFilterItem f) {
        if (f == null) {
            return false;
        }
        if (!f.isNormal()) {
            return true;
        }
        Filter filter = (Filter) f;
        if (filter.getRightIsExpression()) {
            return true;
        }
        
        if (skipNullValueFilter && (filter.getOperator() == Operator.NULL || filter.getOperator() == Operator.NOT_NULL)) {
            return true;
        }
        if (skipNullValueFilter && (filter.getOperator() == Operator.BT)) {
            Object[] values = (Object[]) filter.getRight();
            return validator.checkValue(values[0]) && validator.checkValue(values[1]);
        }
        if (skipNullValueFilter && (! filter.isLogic() && ! validator.checkValue(filter.getRight()))) {
            return false;
        }
        return true;
    }
    
    protected void prepareFilters() {
        if (immutable) throw new IllegalStateException("This instance is immutable");
        
        if(filters == null) {
            filters = new ArrayList<>(5);
        }
    }
    
    protected String appendDot2Prefix(String prefix) {
        if (StringUtils.hasText(prefix) && !prefix.endsWith(".")) {
            return prefix + ".";
        }
        return prefix;
    }
    
    protected boolean hasPrefix(String field) {
        return field != null && field.indexOf(".") != -1;
    }
    
    protected Class<?> getEntityClassByPrefix(String prefix) {
        return entityClass;
    }
    
    protected boolean checkLikeValue(Object value) {
        if (skipNullValueFilter && StringUtils.isEmpty(value)) {
            return false;
        }
        return true;
    }
    
    private void removeEmptyBrackets() {
        int size = filters.size();
        boolean emptyBracket = false;
        int idx = 0;
        for (Iterator<IFilterItem> ite = filters.iterator(); ite.hasNext(); ) {
            IFilterItem fi = ite.next();
            if (!fi.isNormal()) {
                continue;
            }
            Filter f = (Filter) fi;
            if (f.getOperator() == Operator.LEFT_BRACKET) {
                IFilterItem nextFi = filters.get(idx + 1);
                if (nextFi.isNormal()) {
                    Filter next = (Filter) nextFi;
                    emptyBracket = next.getOperator() == Operator.RIGHT_BRACKET;
                    if (emptyBracket) {
                        ite.remove();
                    }
                }
            } else if (emptyBracket) {
                ite.remove();
                emptyBracket = f.getOperator() == Operator.RIGHT_BRACKET;
            }
            idx ++;
        }
        if (size == filters.size()) {
            IFilterItem lastFi = filters.isEmpty() ? null : filters.get(filters.size() - 1);
            if (lastFi != null && lastFi.isNormal()) {
                Filter last = (Filter) lastFi;
                if (last.isLogic() && last.getOperator() != Operator.RIGHT_BRACKET) {
                    filters.remove(filters.size() - 1);
                }
            }
            return;
        }
        removeEmptyBrackets();
    }
    
    protected Object getFilterArgFieldValue(Filter filter, String field) {
        if (filter.getExpressionArgs() != null) {
            return filter.getExpressionArgs().get(field);
        }
        return field;
    }
}
